/*
 * Copyright 2008 Lucas Tan.
 * This copyright notice must always remain intact whenever you use or distribute 
 * the source code contained in this file.
 * Any usage of any code in this file, either in derived work or
 * through the use of external linkage,
 * must always be credited with the author's name.
 * 
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 * 
 * */
using System;
using System.Collections.Generic;
using System.Text;
using System.Xml;
using System.Security.Cryptography;
using System.IO;

namespace PassKeeper.Data
{
	/// <summary>
	/// Represents an account which can hold a collection of accounts.
	/// </summary>
	public sealed class Account : Element
	{
		private const string NameAttributeName = "Name";
		private const string UriAttributeName = "Uri";
		private const string EncryptedElementName = "Encrypted";
		private const string IVAttributeName = "IV";

		/// <summary>
		/// Tag name of this account element.
		/// </summary>
		internal const string ElementName = "Account";

		private AccountCollection _accounts;

		private string _name;
		private Uri _uri;
		private string _username;
		private string _password;
		private string _emailAddress;
		private string _remarks;
		private bool _isDecrypted;

		/// <summary>
		/// Encrypted contents of the protected values, in Base64 encoding.
		/// </summary>
		private string _encryptedStr;

		/// <summary>
		/// Initialization vector in Base64 encoding.
		/// </summary>
		private string _ivStr;

		internal Account(Database owner, string name)
			: base(owner)
		{
			this.Name = name;
			_isDecrypted = true;

			_accounts = new AccountCollection(owner);
			_accounts.Parent = this;
			
			/* All other fields are auto-set to null.*/
		}

		internal Account(Database owner, XmlElement elem)
			: base (owner)
		{
			_accounts = new AccountCollection(owner);
			_accounts.Parent = this;

			/* All other fields are auto-set to null.*/

			Deserialize(elem);
		}

		/// <summary>
		/// Ensures the protected values of this object are fully decrypted and
		/// optionally release the cached encrypted version of the protected values.
		/// </summary>
		internal void EnsureDecrypted(bool releaseCache)
		{
			_accounts.EnsureDecrypted(releaseCache);
			EnsureSelfDecrypted(releaseCache);
		}

		/// <summary>
		/// Clone this object and specify an owner database
		/// for the clone.
		/// </summary>
		/// <param name="ownerDatabase">Owner database of the clone.</param>
		/// <returns>A clone of this object. Will never return null.</returns>
		protected internal override Element Clone(Database ownerDatabase)
		{
			if (ownerDatabase == null)
			{
				throw new ArgumentNullException("ownerDatabase");
			}

			EnsureSelfDecrypted(false);

			Account clonedAccount = new Account(ownerDatabase, _name);
			clonedAccount._username = _username;
			clonedAccount._password = _password;
			clonedAccount._remarks = _remarks;
			clonedAccount._emailAddress = _emailAddress;

			if (_uri != null)
			{
				clonedAccount._uri = new Uri(_uri.OriginalString, UriKind.Absolute);
			}
			else
			{
				clonedAccount._uri = null;
			}

			clonedAccount._accounts = (AccountCollection) _accounts.Clone(ownerDatabase);
			return clonedAccount;
		}

		public override int GetHashCode()
		{
			return AccountCollection.NameEqualityComparer.GetHashCode(_name);
		}

		/// <summary>
		/// Removes this object from its parent.
		/// </summary>
		/// <returns>true if successfully removed; false if this object has no parent.</returns>
		public bool Remove()
		{
			AccountCollection parentAccounts = this.Parent as AccountCollection;

			if (parentAccounts != null)
			{
				return parentAccounts.Remove(_name);
			}

			return false;
		}

		/// <summary>
		/// Load this object with data supplied by an <see cref="XmlElement"/>.
		/// </summary>
		/// <param name="elem">Element to supply the data. This element will not be modified.</param>
		/// <remarks>The state of this object is unmodified if deserialization fails.</remarks>
		/// <exception cref="ArgumentNullException"><paramref name="elem"/> is null.</exception>
		/// <exception cref="ParseException">There is an error in parsing data.</exception>
		public override void Deserialize( XmlElement elem)
		{
			if (elem == null)
			{
				throw new ArgumentNullException("elem");
			}

			string name = elem.GetAttribute(NameAttributeName);
			if (string.IsNullOrEmpty(name))
			{
				throw new MissingAttributeException(elem.Name, NameAttributeName);
			}

			Uri uri = null;
			string uriStr = elem.GetAttribute (UriAttributeName);
			if (!string.IsNullOrEmpty(uriStr))
			{
				try
				{
					uri = new Uri(uriStr, UriKind.Absolute);
				}
				catch (Exception e)
				{
					throw new AttributeParseException(elem.Name, UriAttributeName, e);
				}
			}

			XmlElement encrypted = ParserUtil.GetSingleChild(elem, EncryptedElementName);

			//TODO: check for base64 encoding format error.

			string ivStr = encrypted.GetAttribute(IVAttributeName);
			if (string.IsNullOrEmpty(ivStr))
			{
				throw new MissingAttributeException(encrypted.Name,IVAttributeName);
			}

			string encStr = encrypted.InnerText;
			if (string.IsNullOrEmpty(encStr))
			{
				throw new ElementParseException(encrypted.Name,"Missing encrypted data.");
			}
			
			XmlNodeList nodeList = ParserUtil.GetChildren(elem, _accounts.TagName);
			if (nodeList.Count == 1)
			{
				XmlElement accountsElem = (XmlElement) nodeList[0];
				_accounts.Deserialize(accountsElem);
			}
			else if (nodeList.Count == 0)
			{
				_accounts.Clear();
			}
			else
			{
				throw new ElementParseException(elem.Name,
					string.Format("At most one '{0}' child element allowed.",_accounts.TagName));
			}

			/*Everything's OK!*/
			_name = name;
			_uri = uri;
			_encryptedStr = encStr;
			_ivStr = ivStr;
			_isDecrypted = false;
		}

		/// <summary>
		/// Save the data in this object into an <see cref="XmlElement"/> using the version specified
		/// by the <see cref="P:Database.Version">Version</see> property of this object's owner database.
		/// </summary>
		/// <param name="elem">Element which will hold the data.</param>
		/// <remarks>The state of <paramref name="elem"/> will be unknown if any exception occurs
		/// during the process. However, the state of this object will always be unmodified.</remarks>
		/// <exception cref="ArgumentNullException"><paramref name="elem"/> is null.</exception>
		public override void Serialize( XmlElement elem)
		{
			if (elem == null)
			{
				throw new ArgumentNullException("elem");
			}
			
			elem.SetAttribute(NameAttributeName, _name);

			if (_uri != null)
			{
				elem.SetAttribute(UriAttributeName, _uri.OriginalString);
			}

			string ivStr = null;
			string encryptedStr = this.GetEncryptedContents(out ivStr);

			XmlElement encrypted = elem.OwnerDocument.CreateElement(EncryptedElementName);
			encrypted.InnerText = encryptedStr;
			encrypted.SetAttribute(IVAttributeName, ivStr);
			elem.AppendChild(encrypted);

			XmlElement accountsElem = elem.OwnerDocument.CreateElement(_accounts.TagName);
			_accounts.Serialize(accountsElem);
			elem.AppendChild(accountsElem);
		}

		
		/// <summary>
		/// Gets the encrypted contents of the protected values, in Base64 encoding.
		/// </summary>
		/// <param name="ivStr">Receives the IV, in Base64 encoding, 
		/// used to encrypt the protected values.</param>
		/// <returns>The encrypted protected values, in Base64 encoding. 
		/// Will never return null.</returns>
		private string GetEncryptedContents(out string ivStr)
		{
			if (_encryptedStr != null)
			{
				ivStr = _ivStr;
				return _encryptedStr;
			}

			string[] values = new string[]{
				_username,
				_password,
				_emailAddress,
				_remarks
			};

			MemoryStream ms = new MemoryStream();

			for (int i = 0; i < values.Length; i++)
			{
				string value = (values[i] == null ? string.Empty : values[i]);

				byte[] strbytes = Database.s_UTF8.GetBytes(value);

				ms.Write(GetLEBytes(strbytes.Length), 0, 4);

				if (strbytes.Length != 0)
				{
					ms.Write(strbytes, 0, strbytes.Length);
				}
			}

			byte []data = ms.ToArray();
			ms.Dispose();

			/*Encrypt the data and a new IV will be given to us.*/
			byte[] ivBytes = null;
			byte[] encBytes = this.OwnerDatabase.Encrypt(data, out ivBytes);

			Array.Clear(data, 0, data.Length);

			/*Cache the encrypted data.*/
			_ivStr = ivStr = Convert.ToBase64String(ivBytes, Base64FormattingOptions.None);
			return _encryptedStr = Convert.ToBase64String(encBytes, Base64FormattingOptions.InsertLineBreaks);
		}

		private int GetLEInt32(byte[] data, int index)
		{
			return ((int)(data[index ])) |
				((int)data[index + 1] << 8) |
				((int)data[index + 2] << 16) |
				((int)data[index + 3] << 24);
		}

		private byte[] GetLEBytes(int num)
		{
			return new byte[] { 
			
				(byte)(num ),
				(byte) (num >> 8),
				(byte) (num >> 16),
				(byte) (num >> 24)
			};
		}

		/// <summary>
		/// Ensures that the protected values in this object are decrypted.
		/// </summary>
		/// <param name="releaseCache">Whether to release the cached encrypted version of
		/// the protected values.</param>
		/// <exception cref="CipherException">Error in decrypting the protected values.</exception>
		private void EnsureSelfDecrypted(bool releaseCache)
		{
			if (_isDecrypted)
			{
				if (releaseCache)
				{
					_encryptedStr = null;
					_ivStr = null;
				}
		
				return;
			}

			byte[] ivBytes = null;
			byte[] encBytes = null;
			byte[] data = null;

			try
			{
				ivBytes = Convert.FromBase64String(_ivStr);
				encBytes = Convert.FromBase64String(_encryptedStr);
			}
			catch (Exception e)
			{
				/*Mainly to catch FormatException and possibly other
				 * unexpected ones.*/
				throw new CipherException(false, e);
			}

			data = this.OwnerDatabase.Decrypt(encBytes,ivBytes);

			List<string> valuesList = new List<string>(4);

			/*Start to decrypt the values.*/
			for (int bytesLeft = data.Length, n = 0; bytesLeft >= 4; )
			{
				int valueLength = GetLEInt32(data, n);
				n += 4;
				bytesLeft -= 4;

				if (bytesLeft < valueLength)
				{
					throw new CipherException(false, "Insufficient data");
				}

				if (valueLength == 0)
				{
					valuesList.Add(null);
				}
				else
				{
					string value=null;

					try
					{
						value = Database.s_UTF8.GetString(data, n, valueLength);
					}
					catch (Exception e)
					{
						throw new CipherException(false, e);
					}

					bytesLeft -= valueLength;
					n += valueLength;

					valuesList.Add(value);
				}
			}

			if (valuesList.Count != 4)
			{
				throw new CipherException(false, "Insufficient values");
			}

			Array.Clear(data, 0, data.Length);
		
			/*Note: There's no need to clear 'ivBytes'
			 * and encBytes since they're available publicly.*/

			if (releaseCache)
			{
				_encryptedStr = null;
				_ivStr = null;
			}

			_username = valuesList[0];
			_password = valuesList[1];
			_emailAddress = valuesList[2];
			_remarks = valuesList[3];

			_isDecrypted = true;
		}

		/// <summary>
		/// Gets the account collection of this object.
		/// </summary>
		/// <value>The return value will never be null.</value>
		public AccountCollection Accounts
		{
			get
			{
				return _accounts;
			}
		}

		/// <summary>
		/// Gets the XML tag name for serialization and deserialization.
		/// </summary>
		/// <value>The return value will never be null.</value>
		public override string TagName
		{
			get
			{
				return ElementName;
			}
		}

		/// <summary>
		/// Gets or sets the name.
		/// </summary>
		/// <exception cref="ArgumentException">When setting, the specified
		/// value is either null or empty.</exception>
		/// <remarks>If this object has a parent, the parent will be notified
		/// of the name change.</remarks>
		public string Name
		{
			get
			{
				return _name;
			}
			set
			{
				if (string.IsNullOrEmpty(value))
				{
					throw new ArgumentException("Cannot be null or empty.", "value");
				}

				if (this.Parent != null)
				{
					AccountCollection accounts = (AccountCollection) this.Parent;
					accounts.Remove(_name);
					_name = value;
					accounts.Add(this);
				}
				else
				{
					_name = value;
				}				
			}
		}


		/// <summary>
		/// Gets or sets the URI.
		/// </summary>
		/// <value>When getting, the return value can be null.
		/// When setting, the specified value can be null.</value>
		/// <exception cref="ArgumentException">When setting, the specified
		/// value is not null and is not an absolute URI.</exception>
		public Uri Uri
		{
			get
			{
				return _uri;
			}
			set
			{
				/*Absolute URI is needed because we need to access the 
				 OriginalString property.
				 Also, it is more logical to accept only abs URI.*/
				if (value != null && !value.IsAbsoluteUri)
				{
					throw new ArgumentException("Relative URIs are not accepted.", "value");
				}

				_uri = value;
			}
		}

		/// <summary>
		/// Gets or sets the username.
		/// </summary>
		/// <value>When getting, the return value can be null.
		/// When setting, the specified value can be null.</value>
		public string Username
		{
			get
			{
				EnsureSelfDecrypted(false);
				return _username;
			}
			set
			{
				EnsureSelfDecrypted(true);
				_username = value;
			}
		}


		/// <summary>
		/// Gets or sets the password
		/// </summary>
		/// <value>When getting, the return value can be null.
		/// When setting, the specified value can be null.</value>
		public string Password
		{
			get
			{
				EnsureSelfDecrypted(false);
				return _password;
			}
			set
			{
				EnsureSelfDecrypted(true);
				_password = value;
			}
		}

		/// <summary>
		/// Gets or sets the email address.
		/// </summary>
		/// <value>When getting, the return value can be null.
		/// When setting, the specified value can be null.</value>
		public string EmailAddress
		{
			get
			{
				EnsureSelfDecrypted(false);
				return _emailAddress;
			}
			set
			{
				EnsureSelfDecrypted(true);
				_emailAddress = value;
			}
		}

		/// <summary>
		/// Gets or sets the remarks.
		/// </summary>
		/// <value>When getting, the return value can be null.
		/// When setting, the specified value can be null.</value>
		public string Remarks
		{
			get
			{
				EnsureSelfDecrypted(false);
				return _remarks;
			}
			set
			{
				EnsureSelfDecrypted(true);
				_remarks = value;
			}
		}
	}
}
